#include "bga/system/Tree.h"
#include "bga/math/Matrix.h"

#include "bga/graphics/Scene.h"
#include "bga/graphics/Drawable.h"
#include "bga/graphics/VertexArrayRenderer.h"
#include "bga/graphics/VBORenderer.h"
#include "bga/graphics/CameraFreeFly.h"
#include "bga/graphics/CameraTrackball.h"

#include <cstdlib>  /// for NULL constant

#include <GL/glew.h>
#include <iostream>
#include <cstring>

using namespace std;

/// TODO (chipsman#1#): BUG : Transparency probleme on some pawns


namespace bga
{

Scene::Scene() : camera(NULL), useStereoscopy(false)
{
    /// create uniform which are used to send matrix

    viewMatrixU = new Uniform("viewMatrix", this->camera->getViewMatrix()->getArray(), 4);
    worldMatrixU = new Uniform("worldMatrix", NULL, 4);
    projectionMatrixU = new Uniform("projectionMatrix", this->camera->getProjectionMatrix()->getArray(), 4);
    alphaU = new Uniform("alpha", (float)1.0);

    currentProgram = NULL;
    currentFbo = NULL;

    /*translationStereoMatrix[0] = 1.f;
    translationStereoMatrix[1] = 0.f;
    translationStereoMatrix[2] = 0.f;
    translationStereoMatrix[3] = 0.f;
    translationStereoMatrix[4] = 0.f;
    translationStereoMatrix[5] = 1.f;
    translationStereoMatrix[6] = 0.f;
    translationStereoMatrix[7] = 0.f;
    translationStereoMatrix[8] = 0.f;
    translationStereoMatrix[9] = 0.f;
    translationStereoMatrix[10] = 1.f;
    translationStereoMatrix[11] = 0.f;
    translationStereoMatrix[12] = 0.2f;
    translationStereoMatrix[13] = 0.f;
    translationStereoMatrix[14] = 0.f;
    translationStereoMatrix[15] = 1.f;*/

    translationStereoMatrix(0, 0) = 1.f;
    translationStereoMatrix(0, 1) = 0.f;
    translationStereoMatrix(0, 2) = 0.f;
    translationStereoMatrix(0, 3) = 0.f;
    translationStereoMatrix(1, 0) = 0.f;
    translationStereoMatrix(1, 1) = 1.f;
    translationStereoMatrix(1, 2) = 0.f;
    translationStereoMatrix(1 ,3) = 0.f;
    translationStereoMatrix(2, 0) = 0.f;
    translationStereoMatrix(2, 1) = 0.f;
    translationStereoMatrix(2, 2) = 1.f;
    translationStereoMatrix(2, 3) = 0.f;
    translationStereoMatrix(3, 0) = 0.2f;
    translationStereoMatrix(3, 1) = 0.f;
    translationStereoMatrix(3, 2) = 0.f;
    translationStereoMatrix(3, 3) = 1.f;

    stereoPass = false;
}

Scene::~Scene()
{

    if(this->renderer != NULL)
    {
        delete this->renderer;
        this->renderer = NULL;
    }

    delete viewMatrixU;
    delete worldMatrixU;
    delete projectionMatrixU;
}

void Scene::init()
{
    /// init GLEW
    GLenum err = glewInit();
    if (GLEW_OK != err)
    {
        std::cout << "Error: " << glewGetErrorString(err) << std::endl;
        return;
    }


    /// TODO Bad thing, put it in an object
    if(glewIsSupported("GL_ARB_vertex_buffer_object"))
    {
        this->renderer = new VBORenderer();
    }
    else
    {
        this->renderer = new VertexArrayRenderer();
    }

    //this->camera->glUpdateProjectionMatrix();

    glEnable(GL_DEPTH_TEST);
    glEnable(GL_TEXTURE_2D);
    glEnable(GL_TEXTURE_CUBE_MAP);
}

void Scene::setRoot(SceneNode& node)
{
    this->sceneGraph.setRoot(node);
}

Camera* Scene::getCamera()
{
    return this->camera.get();
}

void Scene::setCamera(Camera* camera)
{
    if(!this->camera)
    {
        this->camera.reset(camera);

        delete this->viewMatrixU;
        delete this->projectionMatrixU;

        this->viewMatrixU = new Uniform("viewMatrix", this->camera->getViewMatrix()->getArray(), 4);
        this->projectionMatrixU = new Uniform("projectionMatrix", this->camera->getProjectionMatrix()->getArray(), 4);
        //delete this->camera;
    }

    //this->camera = camera;
}

void Scene::switchCamera()
{
    if(this->camera.get() != NULL)
    {
        Vector3d position(this->camera->getPosition());
        Vector3d direction(this->camera->getOrientation());


        CameraType::Type type = this->camera->getType();
        /*delete this->camera;
        this->camera = NULL;*/
        switch(type)
        {
        case CameraType::TRACKBALL:
            this->camera.reset(new CameraFreeFly(position, direction, Vector3d(0.0, 1.0, 0.0)));
            break;

        case CameraType::FREEFLY:
            this->camera.reset(new CameraTrackball(position, direction,Vector3d(0.0, 1.0, 0.0)));
            break;

        default:
            break;
        }
    }
}

void Scene::setStereoPass(bool stereoPass)
{
    this->stereoPass = stereoPass;
}

void Scene::setUseStereoscopy(bool useStereoscopy)
{
    this->useStereoscopy = useStereoscopy;
}

void Scene::draw()
{
    this->camera->look();

    /// apply decalage
    if (this->useStereoscopy && this->stereoPass)
    {
        /// TODO : Think about a 3DCamera or a thing like that to handle a different view Matrix
        /// and prevent use from Scene
        Matrix4f* viewMatrix = this->camera->getViewMatrix();
        //float res[16];
        (*viewMatrix) = translationStereoMatrix * (*viewMatrix);
        //Matrix4f::multiplicateMatrix(translationStereoMatrix,viewMatrix,res);
        //memcpy(const_cast<float*>(viewMatrix), res, 16*sizeof(float));
    }

    /// clear transparency and postProcessNodeList node list
    this->transparencyNodeList.clear();
    this->postProcessNodeList.clear();

    SceneGraphIterator iterator = this->sceneGraph.getDepthFirstIterator();

    do
    {
        const SceneNodeElement* sne = iterator.next();

        /// get owner and call process function
        SceneNode* sceneNode = sne->owner;
        sceneNode->process();

        if (sceneNode->getPostProcess())
        {
            configureCurrentStates(sceneNode); /// configure to avoid crash because parent must be configured before
            this->postProcessNodeList.push_back(sceneNode);
        }
        else
        {
            /// configure stencil if this node reflect
            if(sceneNode->getReflect())
            {
                /// configure stencil
                glEnable(GL_STENCIL_TEST);
                glClear(GL_STENCIL_BUFFER_BIT);
                glStencilFunc(GL_ALWAYS, 1, 1);
                glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE);
                /// draw mirror geometry
                glDisable(GL_DEPTH_TEST);
                glEnable(GL_CULL_FACE);
                glCullFace(GL_BACK);
                glColorMask(false, false, false, false);
                drawNode(sceneNode);
                glColorMask(true, true, true, true);
                glEnable(GL_DEPTH_TEST);
                glDisable(GL_CULL_FACE);
                /// we draw children
                glStencilFunc(GL_EQUAL, 1, 1) ;
                glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
                drawReflectedChildren(sceneNode);
                glDisable(GL_STENCIL_TEST);
            }

            /// if this node use blending
            if (sne->alpha < 1.0)
            {
                configureCurrentStates(sceneNode); /// configure to avoid crash because parent must be configured before
                this->transparencyNodeList.push_back(sceneNode);
            }
            else
            {
                /// draw reflect node with blending
                if (sceneNode->getReflect())
                {
                    glEnable(GL_BLEND);
                    glBlendFunc(GL_ONE, GL_DST_ALPHA); /// TODO voir si autre methode, ici skybox ou autre objet doivent etre dessiner apres sinon melange avec leur alpha
                    drawNode(sceneNode);
                    glDisable(GL_BLEND);
                }
                else
                {
                    drawNode(sceneNode);
                }
            }
        }
    }
    while(iterator.hasNext());

    /// draw transparency node after opaque node
    glEnable(GL_BLEND);
    glDepthMask(GL_FALSE);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    drawTransparencyNodeList();
    glDepthMask(GL_TRUE);
    glDisable(GL_BLEND);

    if (this->stereoPass || !this->useStereoscopy)
    {
        /// draw post process node at the end
        drawPostProcessNodeList();
    }
}


void Scene::drawReflectedChildren(SceneNode* node)
{
    node->resetChildrenIterator();
    SceneNode* child = (SceneNode*)(node->getNextChildren());
    while (child != NULL)
    {
        /// active blend in case of child have a blend (WARNING there are not drawn at the end, possible artifact)
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        child->scale(1,-1,1);
        drawNode(child);
        child->scale(1,-1,1);
        glDisable(GL_BLEND);
        child = (SceneNode*)(node->getNextChildren());
    }
}

void Scene::drawTransparencyNodeList()
{
    std::vector<SceneNode*>::iterator it;
    for (it = this->transparencyNodeList.begin(); it != this->transparencyNodeList.end(); ++it)
    {
        drawNode((*it));
    }
}

void Scene::drawPostProcessNodeList()
{
    std::vector<SceneNode*>::iterator it;
    for (it = this->postProcessNodeList.begin(); it != this->postProcessNodeList.end(); ++it)
    {
        drawNode((*it));
    }
}

void Scene::drawNode(SceneNode* node)
{
    configureCurrentStates(node);

    SceneNodeElement* sne = node->getElement();
    if(sne != NULL)
    {
        if(sne->currentProgram != NULL)
        {
            if (sne->currentProgram != currentProgram)
            {
                sne->currentProgram->setActive(true);
                this->currentProgram = sne->currentProgram;
            }
            for (unsigned int i=0; i<sne->currentUniformList.size(); i++)
            {
                sne->currentProgram->sendUniform( *(sne->currentUniformList[i]) );
            }

            /// send current transformation matrix
            sne->currentProgram->sendUniform(*projectionMatrixU);
            sne->currentProgram->sendUniform(*viewMatrixU);
            sne->currentProgram->sendUniform(*worldMatrixU);
            sne->currentProgram->sendUniform(*alphaU);
        }

        for (unsigned int i=0; i<sne->currentTexture.size(); i++)
        {
            if (this->currentTexture.size() < sne->currentTexture.size())
            {
                this->currentTexture.push_back(NULL);
            }
            if (sne->currentTexture[i] != this->currentTexture[i])
            {
                TextureObject::setActiveUnit(i);
                sne->currentTexture[i]->bind();
                this->currentTexture[i] = sne->currentTexture[i];
            }
        }

        if (sne->currentFbo != currentFbo)
        {
            this->currentFbo = sne->currentFbo;
            if (sne->currentFbo != NULL)
            {
                sne->currentFbo->bind();
            }
            else
            {
                glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

            }
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        }

        if (sne->drawable != NULL)
        {
            this->renderer->render(*(sne->drawable));
        }
    }
}

void Scene::configureCurrentStates(SceneNode* currentSceneNode)
{
    SceneNodeElement* sne = currentSceneNode->getElement();

    /// clear SceneNode current uniform list
    sne->currentUniformList.clear();
    /// concat SceneNode uniform list
    for (unsigned int i = 0; i < sne->uniformList.size(); i++)
    {
        sne->currentUniformList.push_back(sne->uniformList[i]);
    }

    /// get parent SceneNode
    SceneNode* parent = (SceneNode*)(currentSceneNode->getParent());
    if (parent == NULL)
    {
        memcpy(sne->currentTransformation.getArray(), sne->transformations.getArray(), 16*sizeof(float));
        sne->currentProgram = sne->program;

        if (stereoPass)
            sne->currentFbo = sne->alternativeFbo;
        else
            sne->currentFbo = sne->fbo;

        /// clear current texture
        sne->currentTexture.clear();
        for (unsigned int i = 0; i < sne->texture.size(); i++)
        {
            sne->currentTexture.push_back(sne->texture[i]);
        }
    }
    else
    {
        SceneNodeElement* parentSne = parent->getElement();

        if (currentSceneNode->getInheritParentTransformation())
        {
            /// compute transformation
            /*Matrix4f::multiplicateMatrix(parentSne->currentTransformation,
                                         sne->transformations,
                                         sne->currentTransformation);*/
            sne->currentTransformation = parentSne->currentTransformation * sne->transformations;
        }
        else
        {
            memcpy(sne->currentTransformation.getArray(), sne->transformations.getArray(), 16*sizeof(float));
        }

        /// concat node uniform with parent node
        for (unsigned int i = 0; i < parentSne->uniformList.size(); i++)
        {
            sne->currentUniformList.push_back(parentSne->currentUniformList[i]);
        }

        /// apply states if not null, else apply parent states
        if (sne->program != NULL)
        {
            sne->currentProgram = sne->program;
        }
        else
        {
            sne->currentProgram = parentSne->currentProgram;
        }

        /// clear current texture
        sne->currentTexture.clear();
        /// put parent textures
        for (unsigned int i = 0; i < parentSne->texture.size(); i++)
        {
            sne->currentTexture.push_back(parentSne->currentTexture[i]);
        }
        /// erase parent texture if necessary
        for (unsigned int i = 0; i < sne->texture.size(); i++)
        {
            if (i < sne->currentTexture.size())
            {
                sne->currentTexture[i] = sne->texture[i];
            }
            else
            {
                sne->currentTexture.push_back(sne->texture[i]);
            }
        }

        if (sne->fbo != NULL)
        {
            if (stereoPass)
                sne->currentFbo = sne->alternativeFbo;
            else
                sne->currentFbo = sne->fbo;
        }
        else
        {
            sne->currentFbo = parentSne->currentFbo;
        }
    }


    /// update transformation uniform
    viewMatrixU->setMatrix(camera->getViewMatrix()->getArray(), 4);
    worldMatrixU->setMatrix(sne->currentTransformation.getArray(), 4);

    /// update alpha uniform
    alphaU->setValue(sne->alpha);
}

}
